﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace Pfz.Factoring
{
	/// <summary>
	/// A factory like component that allows to map a parallel-tree of
	/// components.
	/// For example, the Orm framework maps the Pfz.Expressions into
	/// a parallel tree or Orm Expressions.
	/// Also, you can simple use it as a factory, so you can register
	/// an "editor" for a string, another for an int and so on.
	/// </summary>
	public class ParallelTypeMapper
	{
		private readonly Type[] _parameterTypes = new Type[2];
		private readonly object[] _parameters = new object[2];
		private readonly Dictionary<Type, ParallelTypeMapperCreateDelegate> _dictionaryOfConstructors = new Dictionary<Type, ParallelTypeMapperCreateDelegate>();
		private readonly Dictionary<Type, Type> _genericDictionary = new Dictionary<Type, Type>();

		/// <summary>
		/// Creates a new parallel type mapper instance.
		/// </summary>
		public ParallelTypeMapper()
		{
			_parameterTypes[0] = typeof(ParallelTypeMapper);
			_parameters[0] = this;
		}

		/// <summary>
		/// Adds a source-type that, when mapping a conversion, will call the
		/// given createDelegate. Such delegate can, for example, create
		/// different objects on some conditions, or even always return
		/// the same object (like a singleton instance) on some other cases.
		/// </summary>
		public void Add(Type sourceType, ParallelTypeMapperCreateDelegate createDelegate)
		{
			if (sourceType == null)
				throw new ArgumentNullException("sourceType");

			if (createDelegate == null)
				throw new ArgumentNullException("createDelegate");

			_dictionaryOfConstructors.Add(sourceType, createDelegate);
		}

		/// <summary>
		/// Maps a parallel from a source type to a destination type.
		/// Such destination type needs a constructor that receives a parallel
		/// type mapper and the source object to construct itself.
		/// Generic types are supported, but the same amount of generic
		/// arguments is required on both sides.
		/// </summary>
		public void Add(Type sourceType, Type typeToConstruct)
		{
			if (sourceType == null)
				throw new ArgumentNullException("sourceType");

			if (typeToConstruct == null)
				throw new ArgumentNullException("typeToConstruct");

			bool isGenericTypeDefinition = sourceType.IsGenericTypeDefinition;
			if (isGenericTypeDefinition != typeToConstruct.IsGenericTypeDefinition)
				throw new ArgumentException("Both types must be generic type definitions or none should be.");

			if (isGenericTypeDefinition)
			{
				int count1 = sourceType.GetGenericArguments().Length;
				int count2 = typeToConstruct.GetGenericArguments().Length;

				if (count1 != count2)
					throw new ArgumentException("both types must receive the same number of generic arguments.");

				_genericDictionary.Add(sourceType, typeToConstruct);
			}
			else
			{
				_parameterTypes[1] = sourceType;
				var constructor = typeToConstruct.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, _parameterTypes, null);
				if (constructor == null)
					throw new ArgumentException(typeToConstruct.FullName + " must have a constructor that receives TypeMapper and " + sourceType.FullName);

				var fastDelegate = ReflectionHelper.GetConstructorDelegate(constructor);
				_dictionaryOfConstructors.Add
				(
					sourceType, 
					(alternativeThis, realParameter) => 
					{
						_parameters[1] = realParameter;
						return fastDelegate(_parameters);
					}
				);
			}
		}

		/// <summary>
		/// Gets a parallel instance to the given sourceInstance.
		/// Note that the parallel of null is always null.
		/// </summary>
		public object GetParallelInstance(object sourceInstance)
		{
			if (sourceInstance == null)
				return null;

			var type = sourceInstance.GetType();
			ParallelTypeMapperCreateDelegate createDelegate;
			if (!_dictionaryOfConstructors.TryGetValue(type, out createDelegate))
			{
				if (!type.IsGenericType)
					throw new InvalidOperationException("There is no mapping registered for the given object-type.");

				var genericTypeDefinition = type.GetGenericTypeDefinition();
				if (!_dictionaryOfConstructors.TryGetValue(genericTypeDefinition, out createDelegate))
				{
					Type destinationType;
					if (!_genericDictionary.TryGetValue(genericTypeDefinition, out destinationType))
						throw new InvalidOperationException("There is no mapping registered for the given object-type.");

					var genericArguments = type.GetGenericArguments();
					var madeDestinationType = destinationType.MakeGenericType(genericArguments);

					_parameterTypes[1] = type;
					var constructor = madeDestinationType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, _parameterTypes, null);
					if (constructor == null)
						throw new ArgumentException(madeDestinationType.FullName + " must have a constructor that receives TypeMapper and " + type.FullName);

					createDelegate = ReflectionHelper.GetConstructorDelegate<ParallelTypeMapperCreateDelegate>(constructor);
				}

				_dictionaryOfConstructors.Add(type, createDelegate);
			}

			var result = createDelegate(this, sourceInstance);
			return result;
		}
	}
}
